"""Captcha (Completely Automated Public Turing test to tell Computers and
Humans Apart) module for django

Author: Martin Winkler, June 2007
License: BSD
"""

from os import listdir, sep, access, mkdir, W_OK, R_OK, remove, chmod, path, rmdir, stat
import md5
import tempfile
import datetime
from PIL import Image, ImageColor, ImageFont, ImageDraw
from django.newforms import Widget, Field, ValidationError
from django.conf import settings
from django.utils.translation import gettext
from django.utils.datastructures import MultiValueDict
from django.contrib.captcha import captchasettings, errormsg

def clean_old_entries(captchas_dir, max_age=1200):
    """maintainance function for deleting all expired captchas
    
    captchas_dir: parent directory of the individual image-directories
                 default: [settings.MEDIA_ROOT]/captchas
    max_age:     maximum allowed age of directories in seconds.
                 defaults to 1200 seconds (20 minutes)
    """
    if access(captchas_dir, W_OK):
        basetime = datetime.datetime.now() - datetime.timedelta(seconds=max_age)
        for dname in listdir(captchas_dir):
            d = path.join(captchas_dir, dname)
            if basetime > datetime.datetime.fromtimestamp(stat(d).st_mtime):
                try:
                    for f in listdir(d):
                        remove(path.join(d, f))
                    rmdir(d)
                except:
                    pass

def mycrypt(value, salt):
    return md5.new(value.lower() + salt + settings.SECRET_KEY).hexdigest()

class CaptchaWidget(Widget):
    """generate a captcha image and display the image along with
    a hidden field and a input field"""
    def __init__(self, options=None, seednum=None, *args, **kwargs):
        self.csettings = captchasettings(options, seednum=seednum)
        super(CaptchaWidget, self).__init__(*args, **kwargs)

    def render(self, name, value, attrs=None):
        img = self.generate_image(Image.new('RGB',
                self.csettings['imagesize'],
                self.csettings['bgcolor'])
              )
        self.save_image(img)
        return u'''<input type="hidden" name="%(name)s" value="captcha.%(hiddentext)s"
/><img width="%(imagewidth)s" height="%(imageheight)s" src="%(imageurl)s" alt="" /><br
/><input type="text" name="%(name)s" id="id_%(name)s" />''' % {'name':name,
        'hiddentext': self.hiddentext,
        'imagewidth': self.csettings['imagesize'][0],
        'imageheight': self.csettings['imagesize'][1],
        'imageurl': self.imageurl }

    def generate_image(self, img, seednum=None):
        """ create a image file.
        the filename looks like TMPNAME.HASH.EXT
        where TMPNAME is a system generated temporary name and
        HASH is the hashed solution.
        """
        r = self.csettings['R']
        if self.csettings['auto_cleanup']:
            clean_old_entries(self.csettings['captchas_dir'])

        cs = self.csettings
        imagesize = cs['imagesize']
        fontdir = path.join(cs['captchaconf_dir'], 'fonts')
        fontnames = [path.join(fontdir, x) for x in listdir(fontdir) ]

        for dummy in range(self.csettings['iterations']):
            posnew = 7
            if dummy != 0:
                cs.generate_solution()
            # render characters
            for c in self.csettings['solution']:
                fgimage = Image.new('RGB', imagesize, cs['fgcolor'])
                font = ImageFont.truetype(r.choice(fontnames), r.randrange(*cs['minmaxheight']))
                charimage = Image.new('L', font.getsize(' %s ' % c), '#000000')
                draw = ImageDraw.Draw(charimage)
                draw.text((0,0), ' %s' % c, font=font, fill='#ffffff')
                if cs['eraser']:
                    eraserline = ( 0, r.choice(range(0, charimage.size[1])), 
                            charimage.size[0], r.choice(range(0, charimage.size[1])))
                    draw = ImageDraw.Draw(charimage)
                    draw.line(eraserline, width=cs['eraser_width'] , fill='#000000')
                charimage = charimage.rotate(r.randrange(*cs['minmaxrotations']), expand=1,
                        resample=Image.BILINEAR)
                charimage = charimage.crop(charimage.getbbox())
                maskimage = Image.new('L', imagesize)
                ypos = r.randrange(*cs['minmaxvpos'])
                maskimage.paste(charimage, 
                        (posnew, ypos, 
                            charimage.size[0]+posnew, 
                            charimage.size[1]+ypos)
                        )
                img = Image.composite(fgimage, img, maskimage)
                posnew += charimage.size[0] + r.randrange(*cs['minmaxkerning'])

            # draw line(s)
            for dummy in range(cs.get('num_lines')):
                linex = r.choice( range(2, cs['minmaxheight'][1]) )
                minmaxliney = ( cs['minmaxvpos'][0], 
                        cs['minmaxvpos'][1] + cs['minmaxheight'][0])
                linepoints = [linex, r.randrange(*minmaxliney)]
                while linex < posnew:
                    linex += r.randrange(*cs['minmaxheight']) * 0.8
                    linepoints.append(linex)
                    linepoints.append(r.randrange(*minmaxliney))
                draw = ImageDraw.Draw(img)
                draw.line(linepoints, width=cs['line_width']
                        , fill=cs['fgcolor'])
        return img

    def save_image(self, img):
        """ save file """
        cs = self.csettings
        if not access(cs['captchas_dir'], W_OK):
            mkdir(cs['captchas_dir'])
            chmod(cs['captchas_dir'], 0755)
        dirpath = tempfile.mkdtemp(prefix='c', dir=cs['captchas_dir'])
        chmod(dirpath, 0755)
        self.hiddentext = dirpath[dirpath.rfind(sep)+1:]
        plainfilename = mycrypt(self.csettings['solution'], self.hiddentext)
        self.imageurl = '%s/%s/%s' % (cs['captchabase_url'], 
                self.hiddentext, plainfilename)
        imagepath = path.join(dirpath, plainfilename)
        try:
            img.convert('P', palette=Image.ADAPTIVE, colors=4).save(imagepath + '.gif')
            ext = '.gif'
        except:
            img.save(imagepath + '.jpg')
            ext = '.jpg'
        self.imageurl += ext
        chmod(imagepath + ext, 0644)

    def value_from_datadict(self, data, name):
        if isinstance(data, MultiValueDict):
            return data.getlist(name)
        return data.get(name, None)


class CaptchaField(Field):
    """Field definition for a captcha

    Special options:
    fgcolor: color to use for the letters and line
    bgcolor: color to use for the background of the image
    captchas_dir: file system directory for the captchas
        defaults to [settings.MEDIA_ROOT]/captchas
    captchabase_url: absolute URL of captchas_dir
    captchaconf_dir: file system directory for the captcha module
        contains a "fonts" directory with all available fonts
    auto_cleanup: clean up captchas_dir deleting all entries older
        than 30 minutes
    """

    def __init__(self, required=True, label=None, help_text=None,
            options=None, *args, **kwargs):
        super(CaptchaField, self).__init__(required=required,
            widget=CaptchaWidget(options=options), 
            label=label, help_text=help_text, *args, **kwargs
            )

    def clean(self, value):
        if not isinstance(value, (list, tuple)):
            raise ValidationError(errormsg)
        hiddenval = None
        val = None
        for v in value:
            if v.startswith('captcha.'):
                hiddenval = v[8:]
            else:
                val = ''.join(v.split())
        if not hiddenval:
            raise ValidationError(errormsg)

        imagedir = path.join(self.widget.csettings['captchas_dir'], hiddenval)
        success = False
        for ext in ('.gif', '.jpg', '.png'):
            if access(path.join(imagedir, mycrypt(val, hiddenval) + ext), R_OK):
                success = True

        try:
            for f in listdir(imagedir):
                remove(path.join(imagedir, f))
            rmdir(imagedir)
        except OSError:
            pass

        if not success:
            raise ValidationError(errormsg)
        return True

